using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using SolarWinds.Helpers;

namespace SolarWinds.Effects
{
    /// <summary>
    /// The starfield that renders behind the game, including a parallax effect.
    /// </summary>
    public class Starfield3D : DrawableGameComponent
    {
        #region Constants

        /// <summary>
        /// The number of stars in the starfield.
        /// </summary>
        const int numberOfStars = 512;

        /// <summary>
        /// The number of layers in the starfield.
        /// </summary>
        const int numberOfLayers = 8;

        private const float edgeLength = 1000;

        /// <summary>
        /// The colors for each layer of stars.
        /// </summary>
        static readonly Color[] layerColors = new Color[numberOfLayers] 
                                                  { 
                                                      new Color(255, 255, 255, 255), 
                                                      new Color(255, 255, 255, 216), 
                                                      new Color(255, 255, 255, 192), 
                                                      new Color(255, 255, 255, 160), 
                                                      new Color(255, 255, 255, 128), 
                                                      new Color(255, 255, 255, 96), 
                                                      new Color(255, 255, 255, 64), 
                                                      new Color(255, 255, 255, 32)
                                                  };

        /// <summary>
        /// The background color of the starfield.
        /// </summary>
        static readonly Color backgroundColor = new Color(0, 0, 32);

        /// <summary>
        /// The size of each star, in pixels.
        /// </summary>
        const int starSize = 2;

        #endregion

        #region Gameplay Data

        private readonly Vector3[] mPatchPositions = new Vector3[27];

        private Vector3 mLastShipPosition = Vector3.Zero;
        private Vector3 mShipPostion = Vector3.Zero;
        private Vector3 mStarMovement = Vector3.Zero;
        
        private Vector3 mCenterPatchPosition = Vector3.Zero;


        public Vector3 ShipPosition
        {
            get { return mShipPostion; }
            set
            {
                if (mShipPostion != value)
                {
                    mLastShipPosition = mShipPostion;
                    mShipPostion = value;

                    mStarMovement = 10.0f * (mShipPostion - mLastShipPosition);

                    UpdateVertexData();
                    UpdatePatchCenter();
                }
            }
        }

        private void UpdatePatchCenter()
        {
            // Test if ship move into another patch
            Vector3 newCenterPatchPosition = ShipPosition2PatchPosition(mShipPostion);

            if (mCenterPatchPosition != newCenterPatchPosition)
            {
                mCenterPatchPosition = newCenterPatchPosition;

                UpdatePatches();
            }
        }

        #endregion

        #region Graphics Data

        /// <summary>
        /// The SpriteBatch used to render the starfield.
        /// </summary>
        private SpriteBatch spriteBatch;

        /// <summary>
        /// The texture used for each star, typically one white pixel.
        /// </summary>
        private Texture2D starTexture;

        /// <summary>
        /// The cloud texture that appears behind the stars.
        /// </summary>
        private Texture2D cloudTexture;

        /// <summary>
        /// The effect used to draw the clouds.
        /// </summary>
        private Effect cloudEffect;

        /// <summary>
        /// The parameter on the cloud effect that receives the current position
        /// </summary>
        private EffectParameter cloudEffectPosition;

        private DynamicVertexBuffer mVertexBufferLines;
        private DynamicVertexBuffer mVertexBufferStars;
        private VertexDeclaration mVertexDeclaration;
        private VertexPositionColor[] mVertexData;
        private BasicEffect mBasicEffect;
        private StateBlock mStateBlock;
        private VertexPositionColor[] mStars;

        #endregion


        #region Initialization Methods

        /// <summary>
        /// Create a new Starfield object.
        /// </summary>
        /// <param name="game">The game instance</param>
        public Starfield3D(Microsoft.Xna.Framework.Game game)
            :base(game)
        {
        }

        #region Load / Unload content

        /// <summary>
        /// Load graphics data from the system.
        /// </summary>
        protected override void LoadContent()
        {
            base.LoadContent();

            // load the cloud texture
            cloudTexture = Game.Content.Load<Texture2D>("Textures/Clouds");

            // load the cloud effect
            cloudEffect = Game.Content.Load<Effect>("Shaders/Clouds");
            cloudEffectPosition = cloudEffect.Parameters["Position"];

            // create the star texture
            starTexture = new Texture2D(GraphicsDevice, 
                                        1, 1, 1, TextureUsage.None, SurfaceFormat.Color);
            starTexture.SetData(new Color[] { Color.White });

            // create the SpriteBatch object
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // create vertex buffer for all particles
            mVertexBufferLines = new DynamicVertexBuffer(GraphicsDevice, 
                                                         typeof(VertexPositionColor), numberOfStars*2, BufferUsage.WriteOnly);

            mVertexBufferStars = new DynamicVertexBuffer(GraphicsDevice,
                                                         typeof(VertexPositionColor), numberOfStars, BufferUsage.WriteOnly);

            mVertexDeclaration = new VertexDeclaration(GraphicsDevice, VertexPositionColor.VertexElements);

            // create array for VertexElements
            mVertexData = new VertexPositionColor[numberOfStars*2];
            mStars = new VertexPositionColor[numberOfStars];

            // effect
            mBasicEffect = new BasicEffect(GraphicsDevice, null);
            mBasicEffect.EnableDefaultLighting();

            mStateBlock = new StateBlock(GraphicsDevice);

            CreateRandomStars();
            UpdatePatches();
            UpdateVertexData();
        }

        private void CreateRandomStars()
        {
            const float halfEdgeLength = edgeLength / 2;

            for (int i = 0; i < numberOfStars; ++i)
            {
                mStars[i].Position.X = RandomHelper.RandomBetween(-halfEdgeLength, halfEdgeLength);
                mStars[i].Position.Y = RandomHelper.RandomBetween(-halfEdgeLength, halfEdgeLength);
                mStars[i].Position.Z = RandomHelper.RandomBetween(-halfEdgeLength, halfEdgeLength);
                mStars[i].Color = layerColors[RandomHelper.Random.Next(0, numberOfLayers)];
            }
        }

        /// <summary>
        /// Release graphics data.
        /// </summary>
        protected override void UnloadContent()
        {
            cloudTexture = null;
            cloudEffect = null;
            cloudEffectPosition = null;

            if (starTexture != null)
            {
                starTexture.Dispose();
                starTexture = null;
            }

            if (spriteBatch != null)
            {
                spriteBatch.Dispose();
                spriteBatch = null;
            }
            // free all vertex buffers and delete them
            if (mVertexBufferLines != null)
            {
                mVertexBufferLines.Dispose();
                mVertexBufferLines = null;
            }

            // free all vertex declaration and delete it
            if (mVertexDeclaration != null)
            {
                mVertexDeclaration.Dispose();
                mVertexDeclaration = null;
            }
        }

        #endregion


        private void UpdatePatches()
        {
            const float halfEdgeLength = edgeLength / 2;

            // create the patches
            int j = 0;
            for (int x = -1; x <= 1; ++x)
                for (int y = -1; y <= 1; ++y)
                    for (int z = -1; z <= 1; ++z)
                    {
                        mPatchPositions[j].X = (x + mCenterPatchPosition.X) * halfEdgeLength;
                        mPatchPositions[j].Y = (y + mCenterPatchPosition.Y) * halfEdgeLength;
                        mPatchPositions[j].Z = (z + mCenterPatchPosition.Z) * halfEdgeLength;
                        j++;
                    }
        }

        /// <summary>
        /// Reset the stars and the parallax effect.
        /// </summary>
        public void UpdateVertexData()
        {
            // recreate the stars
            int j = 0;
            for (int i = 0; i < numberOfStars * 2; i+=2)
            {
                mVertexData[i].Position = mStars[j].Position;
                mVertexData[i].Color = mStars[j].Color;

                mVertexData[i + 1].Position.X = mVertexData[i].Position.X + mStarMovement.X;
                mVertexData[i + 1].Position.Y = mVertexData[i].Position.Y + mStarMovement.Y;
                mVertexData[i + 1].Position.Z = mVertexData[i].Position.Z + mStarMovement.Z;
                mVertexData[i + 1].Color = Color.Black;

                j++;
            }
        }

        private static Vector3 ShipPosition2PatchPosition(Vector3 shipPosition)
        {
            Vector3 result = Vector3.Zero;

            result.X = (float)Math.Round(shipPosition.X/edgeLength);
            result.Y = (float)Math.Round(shipPosition.Y/edgeLength);
            result.Z = (float)Math.Round(shipPosition.Z/edgeLength);

            return result;
        }

        #endregion

        #region Draw Methods

        /// <summary>
        /// Draw the starfield.
        /// </summary>
        public override void Draw(GameTime gameTime)
        {
            mStateBlock.Capture();

            // determine the movement vector of the stars
            // -- for the purposes of the parallax effect, 
            //    this is the opposite direction as the position movement.

            // create a rectangle representing the screen dimensions of the starfield
            Rectangle starfieldRectangle = new Rectangle(0, 0, (int)edgeLength, (int)edgeLength);

            // draw a background color for the starfield
            /*spriteBatch.Begin(SpriteBlendMode.None);
            spriteBatch.Draw(starTexture, starfieldRectangle, backgroundColor);
            spriteBatch.End();

            // draw the cloud texture
            spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate,
                              SaveStateMode.None);
            cloudEffect.Begin();
            cloudEffectPosition.SetValue(mPosition);
            cloudEffect.CurrentTechnique.Passes[0].Begin();
            spriteBatch.Draw(cloudTexture, starfieldRectangle, null, Color.White, 0.0f,
                             Vector2.Zero, SpriteEffects.None, 1.0f);
            cloudEffect.CurrentTechnique.Passes[0].End();
            cloudEffect.End();
            spriteBatch.End();*/

            // Set the vertex buffer data to the array of vertices

            GraphicsDevice device = GraphicsDevice;


            // set effect parameters
            mBasicEffect.View = SolarWindsGame.Camera.View;
            mBasicEffect.Projection = SolarWindsGame.Camera.Projection;
            mBasicEffect.VertexColorEnabled = true;
            mBasicEffect.CommitChanges();

            foreach (Vector3 patchPosition in mPatchPositions)
            {
                mBasicEffect.World = Matrix.CreateTranslation(patchPosition);
                mBasicEffect.CommitChanges();

                // draw lines
                mVertexBufferLines.SetData(mVertexData, 0, numberOfStars * 2);
                device.Vertices[0].SetSource(mVertexBufferLines, 0, VertexPositionColor.SizeInBytes);
                device.VertexDeclaration = mVertexDeclaration;

                mBasicEffect.Begin(SaveStateMode.SaveState);
                foreach (EffectPass pass in mBasicEffect.CurrentTechnique.Passes)
                {
                    pass.Begin();
                    device.DrawPrimitives(PrimitiveType.LineList, 0, numberOfStars);
                    pass.End();
                }
                mBasicEffect.End();    

                // draw points
                mVertexBufferStars.SetData(mStars, 0, numberOfStars);
                device.Vertices[0].SetSource(mVertexBufferStars, 0, VertexPositionColor.SizeInBytes);
                device.VertexDeclaration = mVertexDeclaration;

                mBasicEffect.Begin(SaveStateMode.SaveState);
                device.RenderState.PointSize = starSize;
                foreach (EffectPass pass in mBasicEffect.CurrentTechnique.Passes)
                {
                    pass.Begin();
                    device.DrawPrimitives(PrimitiveType.PointList, 0, numberOfStars);
                    pass.End();
                }
                mBasicEffect.End();    
            }


            mStateBlock.Apply();
        }

        #endregion
    }
}